在 Vue 3 中,Composition API 是一種更靈活的狀態管理方式,特別是當應用變得更加複雜時。Pinia 完全支持 Composition API,這意味著我們可以使用 Composition API 來處理狀態邏輯。這篇文章將介紹如何在 Vue 中結合 Pinia 和 Composition API 來實現複雜的狀態管理。
前面幾個單元,給大家介紹比較基本的 vue 應用還有常見方式。現在從這一週的單元開始,會探討較為複雜且會展示一般寫 pinia 的開發人員的 anti pattern
先前 Day4,我們定義一個 store, (檔案: src/stores/counter.ts
)
import { defineStore } from 'pinia';
import { shallowRef, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
// state::
const count = shallowRef(0);
// 定義 getters 作為衍生狀態
const doubleCount = computed(() => count.value * 2);
// 定義 actions 用於修改狀態
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
// 返回狀態、getters 和 actions
return { count, doubleCount, increment, decrement };
});
這裡,我們將 count
狀態定義為 shallowRef
,並使用 computed
來創建一個衍生狀態 doubleCount
。在 actions
中,我們定義了 increment
和 decrement
方法來修改狀態。
在組件中使用 Pinia 和 Composition API
現在,我們可以在 Vue 組件中使用這個 Pinia store,並結合 Composition API 來管理狀態和邏輯。
(檔案: src/components/CounterComponent.vue
)
<script lang="ts" setup>
import { useCounterStore } from '../stores/counter';
const counterStore = useCounterStore();
</script>
<template>
<div>
<p>當前計數:{{ counterStore.count }}</p>
<p>雙倍計數:{{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment">增加</button>
<button @click="counterStore.decrement">減少</button>
</div>
</template>
這裡,我們使用 useCounterStore
來調用 Pinia store,並將狀態和方法解構出來,然後在模板中使用它們。
狀態
在先前範例中我們要抓取 pinia 的 state 的部分,我們要引入 pinia 整個 store
import { useCounterStore } from '../stores/counter';
const counterStore = useCounterStore();
並且要使用 state 時會特別麻煩,總是需要夾帶 counterStore
才可使用
{{ counterStore.count }}
pinia 有提供 storeToRefs
可以成功解構出 state 並且使用上和 ref
或是 shallowRef
無異。
所以以上 前情提要
的部分可以修改為
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useCounterStore } from '../stores/counter';
const { count, doubleCount } = storeToRefs(useCounterStore());
const { increment, decrement } = useCounterStore();
</script>
<template>
<div>
<p>當前計數:{{ count }}</p>
<p>雙倍計數:{{ doubleCount }}</p>
<button @click="increment">增加</button>
<button @click="decrement">減少</button>
</div>
</template>
是不是簡化很多呢?
單向數據流
)在 步驟 2
展示 如何使用 storeToRefs
,而有一招半式會用 pinia 的開發人員大部分學到這裡就會覺得足夠了,可以處理幾乎所有數據的部分。
我們 步驟 3
要展示一般開發人員在 pinia 的使用上不良的操作習慣並且展示如何改善。
我沿用 步驟 2
最後的例子,並加以變化:
(檔案: src/components/CounterComponent.vue
)
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useCounterStore } from '../stores/counter';
const { count, doubleCount } = storeToRefs(useCounterStore());
const { increment, decrement } = useCounterStore();
// 這裡我們在 component 寫一個方法,進行更新 count
const doubleCountInComponent = () => {
count.value *= 2;
}
</script>
<template>
<div>
<p>當前計數:{{ count }}</p>
<p>雙倍計數:{{ doubleCount }}</p>
<button @click="increment">增加</button>
<button @click="decrement">減少</button>
<button @click="doubleCountInComponent">變兩倍!!</button>
</div>
</template>
由上述程式碼可知,我們在 CounterComponent
內增加一個新方法, doubleCountInComponent
去干涉 count
在 pinia 內的狀態。這樣做是可以動且合法的。
但,為何我們要准許這件事合法
因為 vue 非常自由,讓開發人員可以做這樣的操作,也因此在業界產生成千上萬的爛扣(後續天數會有更多例子),
這樣寫的問題是什麼呢?
: 我們舉個例子
假設我們建立了 100 ,1000 像這樣的元件,可能那些元件會行使某些功能去更動到 useCounterStore
的 count
也許更動的方式是某些元件裡的 ref 因為 watch 順便更新了 count
還是寫了一個複雜的非同步動作,更新了 count
,如果只有一個還好判斷,如果事件驅動一整串粽子 讓某個狀態 A 變成 B 又變成 A 並且在非同步執行結束又讓 A 變成 C,那我們怎判斷是哪裡的 bug ?
是哪個元件?還是寫在哪個 composables 裡面?
為了確保狀態可做管理,所以我建議 所有資料的更新請在 pinia 的 store method 裡面做
因此我們可以把以上程式碼改成這樣
(檔案: src/stores/counter.ts
)
import { defineStore } from 'pinia';
import { shallowRef, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
// state::
const count = shallowRef(0);
// 定義 getters 作為衍生狀態
const doubleCount = computed(() => count.value * 2);
// 定義 actions 用於修改狀態
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
// 我們把方法寫道 pinia 的 store 裡面
const doubleCountInComponent = () => {
count.value *= 2;
}
// 返回狀態、getters 和 actions
return { count, doubleCount, increment, decrement };
});
(檔案: src/components/CounterComponent.vue
)
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useCounterStore } from '../stores/counter';
const { count, doubleCount } = storeToRefs(useCounterStore());
const { increment, decrement, doubleCountInComponent } = useCounterStore(); // <-- 我們引用 doubleCountInComponent
// 這時我們這個方法就不需要了
// const doubleCountInComponent = () => {
// count.value *= 2;
//}
</script>
<template>
<div>
<p>當前計數:{{ count }}</p>
<p>雙倍計數:{{ doubleCount }}</p>
<button @click="increment">增加</button>
<button @click="decrement">減少</button>
</div>
</template>
這樣並沒有結束,我們仍然無法預期同事會不會照規範走,也許有其他地方你的同事在 compoent 去進行操作 state 本身的操作
樹大必有枯枝,人多必有白癡
因此我們要多一個強而有力的規範確保單向資料流的操作,正好 vue 原生有提供可以確保資料是唯獨狀態的方法readonly
因此我們可以把 Pinia Store 的部分改成
(檔案: src/stores/counter.ts
)
import { defineStore } from 'pinia';
import { shallowRef, computed, readonly } from 'vue';
export const useCounterStore = defineStore('counter', () => {
// state::
const count = shallowRef(0);
// 定義 getters 作為衍生狀態
const doubleCount = computed(() => count.value * 2);
// 定義 actions 用於修改狀態
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
// 我們把方法寫道 pinia 的 store 裡面
const doubleCountInComponent = () => {
count.value *= 2;
}
// 返回狀態、getters 和 actions
return {
count: readonly(count),
// 加了這行後,這樣有開發人員在使用 pinia 以外的地方更改 state 就會跳出警告,且無法更動
doubleCount,
increment,
decrement
};
});
完成單向資料流。
有些開發人員,會在 composable
和 pinia
之間選擇,不外乎是考慮幾個要點
這些狀態都是使用 pinia 的標準。
不過我們必須要謹慎且更近一步的去思考這些問題
個人使用 pinia store 的哲學在於,管理狀態如其名 pinia 屬於 state management
的部分
同時也要想到 vue 提供的 dev tool 是可以監測 pinia 的狀態的,這樣可以減少很多除錯的時間,包含更改狀態時可以直接對 dev tool 做操作(如圖)
另一個部分是:
用過即丟的狀態 -> 我們會選擇 composables
需要保持(或可能需要保持)的狀態 -> 我們會選擇 pinia
通過結合 Pinia 和 Vue 的 Composition API,我們可以靈活且高效地管理應用中的複雜狀態。使用 Composition API 讓我們能夠更自然地組織狀態和邏輯,Pinia 的簡潔 API 提供了更好的開發體驗。
這篇文章展示了如何使用 Composition API 來定義 Pinia store,並涵蓋了如何處理異步操作和模組化管理。希望這篇文章能幫助你更好地理解 Vue 中的狀態管理。